package com.eobard.obfuscate.core;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.eobard.adapter.BaseVisitorAdapter;
import com.eobard.config.obfuscate.ObfuscatorConfig;
import com.eobard.obfuscate.IObfuscator;
import com.eobard.params.RandomRenamePackageParams;
import com.eobard.utils.CollectionUtils;
import com.eobard.utils.ExceptionUtils;
import com.eobard.utils.FileUtils;
import com.eobard.utils.ObfuscatorUtils;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.ImportDeclaration;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.function.Consumer;

/**
 * @Author: Eobard Gu
 * @Date: 2025-08-28
 * @Description: 随机重命名包名混淆器
 */
@Component
@Slf4j
public class RandomRenamePackageObfuscator extends BaseVisitorAdapter implements IObfuscator {

    @Value("${obfuscate.enable.typeSolver.logging:false}")
    private boolean print;
    //源码路径
    private static String sourceRoot;

    //基础父包名
    private static String basePackage;

    //需要排除的包名集合(包括该包的子包)
    private static Set<String> EXCLUDE_PACKAGES;

    // 旧包名->新包名的映射
    private static Map<String, String> packageMapping = new LinkedHashMap<>();

    //已经生成过的包名缓存
    private static List<String> generatedPackageCache = new ArrayList<>();

    //数据初始化
    @Override
    public void initialize(ObfuscatorConfig config) {
        //清除上一次的记录
        EXCLUDE_PACKAGES = null;
        CollectionUtils.clear(packageMapping);

        //如果sourceRoot没有以/结尾,那就手动加上
        RandomRenamePackageParams params = config.getRandomRenamePackageParams();
        String sr = config.getSourceRoot();
        String bp = params.getBasePackage();

        ExceptionUtils.checkNonNull(
                List.of(sr, bp),
                List.of("sourceRoot不能为空", "basePackage不能为空")
        );

        sourceRoot = sr.endsWith(File.separator) ? sr : sr + File.separator;

        basePackage = bp.replace(File.separatorChar, '.');

        if (CollectionUtil.isEmpty(params.getExcludePackages())) {
            EXCLUDE_PACKAGES = Collections.emptySet();
        } else {
            EXCLUDE_PACKAGES = params.getExcludePackages();
            //检查排除的包是否存在
            FileUtils.checkDirectoryExist(EXCLUDE_PACKAGES, sourceRoot);
        }

        BaseVisitorAdapter.initSolver(sourceRoot, null);

    }

    //核心混淆代码
    @Override
    public void obfuscate(ObfuscatorConfig config) {
        //获取基础包所在的完整路径
        Path baseDir = Paths.get(sourceRoot + basePackage.replace(".", File.separator));
        if (!Files.exists(baseDir) || !Files.isDirectory(baseDir)) {
            throw new IllegalStateException("基础包路径不存在: " + baseDir);
        }

        //规范化包名
        Set<String> normalizedExcludes = ObfuscatorUtils.normalizedPackage(EXCLUDE_PACKAGES);

        //1.收集baseDir路径下所有的包及其子包,并按照路径长度从长到短排序
        List<Path> allDirs = null;
        try {
            allDirs = Files.walk(baseDir)
                    .filter(Files::isDirectory)
                    .sorted(Comparator.comparingInt(p -> -p.toString().length()))
                    .toList();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        //2.遍历基础包下所有的包及其子包,并将其移动到随机的包中
        allDirs.stream()
                //如果当前包路径和基础包所在的路径一致,那么就跳过自身basePackage的重命名
                .filter(dir -> !dir.equals(baseDir))
                .filter(dir -> {
                    //获取当前dir相较于baseDir的相对路径,eg: relativePathDots=service.impl即直接获取basePackage下面的子包名称
                    String currentPkg = baseDir.relativize(dir).toString().replace(File.separatorChar, '.');
                    //获取完整的包名称, eg:com.eobard.service.impl
                    String oldPkg = basePackage + "." + currentPkg;
                    //如果当前的完整包名称是属于排除的包集合中的话,那么就需要跳过该包
                    boolean anyMatch = normalizedExcludes.stream().anyMatch(pkg -> oldPkg.equals(pkg) || oldPkg.startsWith(pkg + "."));
                    if (anyMatch) {
                        if (print) {
                            log.error("skipping package:[{}],because it belongs to {}", oldPkg, normalizedExcludes);
                        }
                        return false;
                    }
                    return true;
                })
                .forEach(dir -> {
                    // 获取当前dir相对于baseDir的相对路径
                    String currentPkg = baseDir.relativize(dir).toString().replace(File.separatorChar, '.');
                    String oldPkg = basePackage + "." + currentPkg;

                    // 生成随机包名
                    String randomPackageName = randomPackageName(6);
                    //获取当前包的上一层文件
                    Path parent = dir.getParent();
                    String parentSuffix = "";
                    //如果当前包的父路径不是baseDir,那么就可以更改包名
                    if (parent != null && !parent.equals(baseDir)) {
                        String parentRel = baseDir.relativize(parent).toString().replace(File.separatorChar, '.');
                        parentSuffix = "." + parentRel;
                    }
                    //替换当前包的名称,重新拼接新的包:利用上一层包的全部路径+新的随机包名
                    String newPkg = basePackage + parentSuffix + "." + randomPackageName;
                    // 移动目录
                    Path newDir = dir.resolveSibling(randomPackageName);
                    try {
                        Files.move(dir, newDir, StandardCopyOption.REPLACE_EXISTING);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }

                    // 保存映射关系
                    packageMapping.put(oldPkg, newPkg);
                    if (print) {
                        log.info("包 【{}】 已改为 【{}】", oldPkg, newPkg);
                    }
                });

        //3.整理旧-新包的映射关系,先对子包进行替换,在对父包进行替换
        List<Map.Entry<String, String>> sortedPkgMappings = packageMapping
                .entrySet()
                .stream()
                .sorted((e1, e2) -> Integer.compare(e2.getKey().length(), e1.getKey().length()))
                .toList();


        //4.对所有java源文件的import和package进行替换
        Consumer<? super Path> action = path -> {
            try {
                CompilationUnit cu = StaticJavaParser.parse(path);
                //4.1 对所有的import替换
                cu.findAll(ImportDeclaration.class).forEach(imp -> {
                    //获取导包的名称
                    final String[] name = {imp.getNameAsString()};
                    sortedPkgMappings.forEach(entry -> {
                        //获取旧的包名
                        String oldPkg = entry.getKey();
                        //获取新的包名
                        String newPkg = entry.getValue();
                        //如果当前导的包名和旧的相似,那么旧替换成新的
                        if (name[0].startsWith(oldPkg)) {
                            imp.setName(name[0].replace(oldPkg, newPkg));
                            name[0] = imp.getNameAsString();
                        }
                    });
                });

                //4.2 对所有的package声明替换
                cu.getPackageDeclaration().ifPresent(pkg -> {
                    //包名
                    final String[] pkgName = {pkg.getNameAsString()};
                    sortedPkgMappings.forEach(map -> {
                        //旧的包名
                        String oldPkg = map.getKey();
                        //新的包名
                        String newPkg = map.getValue();
                        //如果当前包的声明是和旧的包名相似,那么就替换
                        if (pkgName[0].startsWith(oldPkg)) {
                            pkg.setName(pkgName[0].replace(oldPkg, newPkg));
                            pkgName[0] = pkg.getNameAsString();
                        }
                    });
                });

                //TODO:如果需要替换“全限定类名”(eg: com.eobard.utils.PageUtils.xxx这种形式)，可遍历Name、ClassOrInterfaceType 等节点
//            cu.findAll(Name.class).forEach(name -> {
//                            if (name.getParentNode().isPresent() && name.getParentNode().get() instanceof Name) {
//                                return;
//                            }
//                        });

//            cu.findAll(ClassOrInterfaceType.class).forEach(type -> {
//            });

                //4.3 写出文件
                Files.write(path, cu.toString().getBytes());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        };
        FileUtils.loopFiles(sourceRoot, action);
    }

    @Override
    public boolean supports(ObfuscatorConfig config) {
        return ObjectUtil.isNotEmpty(config.getRandomRenamePackageParams());
    }


    //生成随机包名
    private static String randomPackageName(int len) {
        //需要判断,如果存在的话，需要再次生成一次
        String randomString = RandomUtil.randomString(RandomUtil.BASE_CHAR_NUMBER, Math.max(0, len));

        //如果包的第一位是数字或者以z开头那么都重新生成一次
        while (Character.isDigit(randomString.charAt(0)) || randomString.charAt(0) == 'z' || randomString.charAt(0) == 'Z') {
            randomString = RandomUtil.randomString(RandomUtil.BASE_CHAR_NUMBER, Math.max(0, len));
        }

        //只要缓存中有,那就要重新生成包名,防止出现相同的包名
        while (generatedPackageCache.contains(randomString)) {
            randomString = RandomUtil.randomString(RandomUtil.BASE_CHAR_NUMBER, Math.max(0, len));
        }
        //说明没出现过那就放入缓存中
        generatedPackageCache.add(randomString);
//        return "ZZ" + randomString;
        return randomString;
    }
}
